/* Alloy Analyzer 4 -- Copyright (c) 2007-2008, Derek Rayside
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files
 * (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify,
 * merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
 * OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package edu.mit.csail.sdg.alloy4viz;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import edu.mit.csail.sdg.alloy4.ConstList;
import edu.mit.csail.sdg.alloy4.Util;
import edu.mit.csail.sdg.alloy4graph.DotColor;

/** This class implements the automatic visualization inference.
 *
 * <p><b>Thread Safety:</b> Can be called only by the AWT event thread.
 */

final class MagicLayout {

   /** The VizState object that we're going to configure. */
   private final VizState vizState;

   private Set<AlloyType> enumerationTypes = new LinkedHashSet<AlloyType>();
   private Set<AlloyType> singletonTypes = new LinkedHashSet<AlloyType>();
   private AlloyType projectionType = null;
   private Set<AlloyRelation> spineRelations = Collections.emptySet();

   /** Constructor. */
   private MagicLayout(final VizState vizState) { this.vizState = vizState; }

   /** Main method to infer settings. */
   public static void magic(final VizState vizState) {
      vizState.resetTheme();
      final MagicLayout st = new MagicLayout(vizState);
      st.identifyEnumerationTypes();
      st.projection();
      st.nodeVisibility();
      st.spine();
      st.attributes();
      st.edgeLabels();
   }

   /** SYNTACTIC: An enumeration follows the pattern "abstract sig Colour; one sig Red; one sig Blue".
    */
   private void identifyEnumerationTypes() {
      final AlloyModel model = vizState.getCurrentModel();
      final Set<AlloyType> types = model.getTypes();
      for (final AlloyType t : types) {
         if (enumerationTypes.contains(t)) continue; // we've already checked this one, don't muck with it now
         if (t.isOne) singletonTypes.add(t);
         if (!t.isBuiltin && t.isAbstract) {
            List<AlloyType> subTypes = model.getSubTypes(t);
            int numberOfSingletonSubtypes = 0;
            for (AlloyType st: subTypes) {
               if (st.isOne) { numberOfSingletonSubtypes++; singletonTypes.add(st); }
            }
            if (subTypes.size() == numberOfSingletonSubtypes) { // we have a winner!
               enumerationTypes.add(t);
               enumerationTypes.addAll(subTypes);
               for (final AlloyType st: subTypes) {
                  // all of the subtypes in the enumeration should have visibility inherited
                  // so that the user only needs to make the abstract supertype visible if we made a mistake hiding these things
                  vizState.nodeVisible.put(st, null);
               }
               // hide unless these are the source of some relation
               boolean visible = false;
               for (AlloyRelation r : model.getRelations()) {
                  AlloyType sourceType = r.getTypes().get(0);
                  if (t.equals(sourceType) || subTypes.contains(sourceType)) { visible = true;  break; }
               }
               vizState.nodeVisible.put(t, visible); // log("VizInference: visible status of enumeration type " + t + " " + visible);
            }
         }
      }

   }

   /** SEMANTIC/LAYOUT: Determine at most one relation to project over.
    *
    * When do we project over a sig? Do we ever project over more than one?
    * <ul>
    * <li> pick 0 or 1 things to project over
    * <li> match names: Time, State, Tick, TimeStep
    * <li> if ord is opened over the sig
    * <li> if present in several ternary relations (i.e. if it will help viz
    * high arity relations)
    * <li> position in relation (always end or always beginning)
    * <li> should we try to catch projections such as the one over birthday
    * books?
    * <li> pattern match (b,b') to catch transition relations
    * <li> add combo box in GUI (?)
    * </ul>
    */
   private void projection() {
      // only fiddle with this if it hasn't already been set somewhere else
      if (projectionType == null && vizState.getProjectedTypes().isEmpty()) {
         AlloyModel model = vizState.getCurrentModel();
         //final Set<AlloyType> candidateTypes = new HashSet<AlloyType>();
         Map<AlloyType,Integer> scores = new LinkedHashMap<AlloyType,Integer>();
         for (AlloyType t : model.getTypes()) {
            scores.put(t, 0);
            // does it have a name like State, Time, etc
            if (hasLikelyProjectionTypeName(t.getName())) {
               scores.put(t, scores.get(t)+1 );
            }
            // is it in some ternary relation?
            for (AlloyRelation r : model.getRelations()) {
               if (r.getArity() > 2 && r.getTypes().contains(t))  scores.put(t, scores.get(t)+1 );
            }
         }
         // now we have the scores, see who the winners are:
         int max = 0;
         final Set<AlloyType> winners = new LinkedHashSet<AlloyType>();
         for (final Map.Entry<AlloyType,Integer> e : scores.entrySet()) {
            if (e.getValue() == max) winners.add(e.getKey());
            if (e.getValue() > max) { max = e.getValue();  winners.clear();  winners.add(e.getKey()); }
         }
         if (max < 2) {
            // no winner, don't project
            // log("VizInference: no candidate type to project on.");
         } else {
            if (winners.size() > 1) {
               // we have a tie ... what to do?
               // log("VizInference: projection tie. " + winners);
            }
            final AlloyType winner = winners.iterator().next();
            // pick one arbitrarily for now ...
            // log("VizInference: projecting on " + max + " " + winner);
            projectionType = winner;
            vizState.project(projectionType);
         }

      }
   }

   private final static ConstList<String> LIKELY_PROJECTION_TYPE_NAMES = Util.asList("State", "TrainState", "Time", "Tick", "TimeStep");

   private final boolean hasLikelyProjectionTypeName(final String n) {
      for(String s: LIKELY_PROJECTION_TYPE_NAMES) if (n.startsWith(s) || n.endsWith(s)) return true;
      return false;
   }

   /** SEMANTIC/LAYOUT: Determine some relations to be the spine (ie, influence
    * the layout).
    *
    * Which relations should be used to layout? all? none? clever?
    * <ul>
    * <li> interesting example: 2d game grid
    * <li> ex: toplogical sort -- layout tree and list, not cnxn between them
    * <li> look for homogenius binary relation (a -&gt; a)
    * <li> may be several relations defining the spine
    * </ul>
    *
    */
   private void spine() {
      AlloyModel model = vizState.getCurrentModel();
      Set<AlloyRelation> relations = model.getRelations();
      if (!relations.isEmpty()) {
         // only mess with the relations if there are some
         // only binary relations are candidates
         Set<AlloyRelation> spines = new LinkedHashSet<AlloyRelation>();
         for (AlloyRelation r : relations) {
            if (r.getArity() == 2) {
               List<AlloyType> rtypes = r.getTypes();
               AlloyType targetType = rtypes.get(1);
               // only a spine if the target is not an enumeration type
               if (!enumerationTypes.contains(targetType)) { spines.add(r); }
               // however, binary relations named parent should be layed out backwards
               if (r.getName().equals("parent")) { vizState.layoutBack.put(r, true); }
            }
         }
         // do we have any spines? if so, use them, if not use all relations
         spineRelations = spines.isEmpty() ? relations : spines;
      }
      // set everything to not influence layout
      for (AlloyRelation r : relations) {
         vizState.constraint.put(r, false);
         vizState.edgeColor.put(r, DotColor.GRAY);
      }
      // set spines to influence layout
      for (AlloyRelation s : spineRelations) {
         vizState.constraint.put(s, null);
         // inherit the default color, which should be black
         vizState.edgeColor.put(s, null);
      }

   }

   /** SEMANTIC/LAYOUT: Determine whether non-projection, non-spine relations
    * should be shown as attributes or edges.
    *
    * <ul>
    * <li> binary vs. higher arity -- only make binary attributes
    * <li> use attributes on-demand to reduce clutter, not blindly
    * <li> functional relations should be attributes (what about a tree?)
    * <li> never make something an edge and an attribute
    *
    * </ul>
    *
    */
   private void attributes() {
      AlloyModel model = vizState.getCurrentModel();
      for (AlloyRelation r : model.getRelations()) {
         List<AlloyType> rTypes = r.getTypes();
         if (r.getArity()==2 && !rTypes.contains(projectionType) && !spineRelations.contains(r)) {
            // it's binary, non-projection and non-spine
            AlloyType targetType = rTypes.get(1);
            if (enumerationTypes.contains(targetType)) {
               // target is an enumeration: we have an attribute
               vizState.attribute.put(r, true);
               vizState.edgeVisible.put(r, false);
            }
         }
      }
   }


   /** PRESENTATIONAL: Labels for edges. */
   private void edgeLabels() {
      AlloyModel model = vizState.getCurrentModel();
      int relationsAsEdges = 0;
      AlloyRelation visibleRelation = null;
      for (AlloyRelation r : model.getRelations()) {
         Boolean v = vizState.edgeVisible.get(r);
         if (v == null || v.booleanValue()) {
            // it's visible
            relationsAsEdges++;
            visibleRelation = r;
            // remove text before last slash
            MagicUtil.trimLabelBeforeLastSlash(vizState, r);
         }
      }
      // If there's only one relation visible as an edge, and it's binary, then no need to label it.
      if (1 == relationsAsEdges && visibleRelation.getArity()==2) {
         vizState.label.put(visibleRelation, "");
      }
   }


   /** SYNTACTIC/VISUAL: Hide some things. */
   private void nodeVisibility() {
      AlloyModel model = vizState.getCurrentModel();
      Set<AlloyType> types = model.getTypes();
      for (AlloyType t: types) if (!t.isBuiltin && MagicUtil.isActuallyVisible(vizState, t) && t.getName().endsWith("/Ord")) {
         vizState.nodeVisible.put(t, false);
      }
      for (AlloySet s: model.getSets()) if (MagicUtil.isActuallyVisible(vizState, s) && s.getName().endsWith("/Ord")) {
         vizState.nodeVisible.put(s, false);
      }

   }
}
